// 
// SoapTrace.Net - A soap message monitoring utility. 
// 
// Copyright (C) 2004 Matthew Ward
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
// 
// Matthew Ward (mrward@users.sourceforge.net)
// 

using System;
using System.Globalization;
using System.Diagnostics;
using System.Text;
using System.Text.RegularExpressions;

namespace SoapTrace.Core.Communication
{
	/// <summary>
	/// The header of a Soap Message.  This is the base class that
	/// <see cref="SoapResponseHeader"/> and <see cref="SoapRequestHeader"/> derive
	/// from.
	/// </summary>
	public class SoapHeader
	{
		/// <summary>
		/// The http header bytes.
		/// </summary>
		private ByteArray m_HeaderBytes = new ByteArray( );

		/// <summary>
		/// The number of CR and LF bytes received.  Used to determine
		/// whether the last byte of the header has been received.
		/// </summary>
		private int m_EndOfHeaderByteCount;

		/// <summary>
		/// The http header.
		/// </summary>
		private string m_Header = String.Empty;

		/// <summary>
		/// The length of the Http message body.
		/// </summary>
		private int m_ContentLength;

		/// <summary>
		/// Status flag indicating whether the http message body will contain 
		/// chunks.
		/// </summary>
		private bool m_Chunked;

		/// <summary>
		/// The fields in the header.
		/// </summary>
		private SoapHeaderFieldsCollection m_Fields = new SoapHeaderFieldsCollection( );

		/// <summary>
		/// Characters to be trimmed from the end of a header line.
		/// </summary>
		private char[ ] m_LineEndCharacters = new char[ ] { '\r', '\n' };

		/// <summary>
		/// The first line of the http header.
		/// </summary>
		private string m_StartLine = String.Empty;
		
		/// <summary>
		/// Carriage return character.
		/// </summary>
		public const byte CR = 13;

		/// <summary>
		/// Line feed character.
		/// </summary>
		public const byte LF = 10;

		/// <summary>
		/// The Http header's content length field name.
		/// </summary>
		public const string ContentLengthFieldName = "Content-Length";

		/// <summary>
		/// The Http header's transfer encoding field name.
		/// </summary>
		public const string TransferEncodingFieldName = "Transfer-Encoding";
		
		/// <summary>
		/// The Http header's transfer encoding chunked field name.
		/// </summary>
		public const string TransferEncodingChunked = "chunked";

		/// <summary>
		/// Creates a new instance of the <see cref="SoapHeader"/> class.
		/// </summary>
		public SoapHeader( )
		{
		}

		/// <summary>
		/// Gets the soap header as a string.
		/// </summary>
		public string Header
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_Header;
			}
		}

		/// <summary>
		/// Gets the length of the Http message body's content in bytes.
		/// </summary>
		public int ContentLength
		{
			get
			{
				return m_ContentLength;
			}
		}

		/// <summary>
		/// Gets whether the Http message body will contain chunks.
		/// </summary>
		public bool IsChunked
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_Chunked;
			}
		}

		/// <summary>
		/// Gets the first line of the Http header.
		/// </summary>
		public string StartLine
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_StartLine;
			}
		}

		/// <summary>
		/// Checks the first line of the data bytes to see if it is a valid
		/// Http request.
		/// </summary>
		public static bool IsHttpStartLine( byte[ ] bytes )
		{
			bool IsValid = false;
			
			// Find first carriage return.
			
			int Index = FindCarriageReturn( bytes );
			
			if( Index > 0 )
			{
				string StartLine = UnicodeEncoding.UTF8.GetString( bytes, 0, Index );
				
				IsValid = Regex.IsMatch( StartLine, @"^(OPTIONS|GET|HEAD|POST|PUT|DELETE|TRACE) ([*;/?:%@&=+\.\-\w\d]*)+ HTTP/\d+.\d+$" );
			}
			
			return IsValid;
		}
		
		/// <summary>
		/// Gets the soap header fields.
		/// </summary>
		public SoapHeaderFieldsCollection Fields
		{
			[ DebuggerStepThrough ]
			get
			{
				return m_Fields;
			}
		}

		/// <summary>
		/// Adds and parses a byte of the header.
		/// </summary>
		/// <param name="messageByte">A header byte.</param>
		/// <returns>A status flag indicating whether the last
		/// byte of the header was received.</returns>
		public virtual bool AddByte( byte messageByte )
		{
			m_HeaderBytes.Add( messageByte );

			bool EndOfHeader = false;

			if( messageByte == SoapHeader.CR )
			{
				if( ( m_EndOfHeaderByteCount == 0 ) || ( m_EndOfHeaderByteCount == 2 ) )
				{
					++m_EndOfHeaderByteCount;
				}
				else
				{
					m_EndOfHeaderByteCount = 0;
				}
			}
			else if( messageByte == SoapHeader.LF )
			{
				if( ( m_EndOfHeaderByteCount == 1 ) || ( m_EndOfHeaderByteCount == 3 ) )
				{
					++m_EndOfHeaderByteCount;
				}
				else
				{
					m_EndOfHeaderByteCount = 0;
				}
			}
			else
			{
				m_EndOfHeaderByteCount = 0;
			}

			if( m_EndOfHeaderByteCount == 4 )
			{
				EndOfHeader = true;

				m_Header = m_HeaderBytes.GetUTF8String( );

				ParseHeader( );
			}

			return EndOfHeader;
		}

		/// <summary>
		/// Clears the contents of the header.
		/// </summary>
		public void Clear( )
		{
			m_HeaderBytes.Clear( );
			m_EndOfHeaderByteCount = 0;
			m_Header = String.Empty;
			m_ContentLength = 0;
			m_Fields.Clear( );
		}
		
		/// <summary>
		/// Creates a new header based on any changes
		/// made to the fields.
		/// </summary>
		public void Regenerate( )
		{
			StringBuilder HeaderText = new StringBuilder( );
			
			HeaderText.Append( m_StartLine );
			HeaderText.Append( "\r\n" );
			
			foreach( SoapHeaderField Field in m_Fields )
			{
				HeaderText.Append( Field.Name );
				HeaderText.Append( ": " );
				HeaderText.Append( Field.Value );
				HeaderText.Append( "\r\n" );
			}
			
			HeaderText.Append( "\r\n" );
			
			m_Header = HeaderText.ToString( );
		}

		/// <summary>
		/// Parses the header fields and interprets some of the field values.
		/// </summary>
		private void ParseHeader( )
		{
			ParseFields( );

			if( m_Fields.Count > 0 )
			{
				GetContentLength( );
				GetTransferEncoding( );
			}
		}

		/// <summary>
		/// Extracts the various field values from the http header.
		/// </summary>
		private void ParseFields( )
		{
			// Remove CRLF from end of header string.

			string Header = m_Header.Trim( m_LineEndCharacters );

			string[ ] Fields = Header.Split( '\n' );

			if( Fields.Length > 0 )
			{
				m_StartLine = Fields[ 0 ].Substring( 0, Fields[ 0 ].Length - 1 );

				for( int i = 1; i < Fields.Length; ++i )
				{
					string Field = Fields[ i ];
					SoapHeaderField HeaderField = new SoapHeaderField( Field );

					m_Fields.Add( HeaderField );
				}
			}
		}

		/// <summary>
		/// Gets the Http message body size in bytes.
		/// </summary>
		private void GetContentLength( )
		{
			SoapHeaderField HeaderField = m_Fields.Find( SoapHeader.ContentLengthFieldName );
			
			if( HeaderField != null )
			{
				m_ContentLength = Convert.ToInt32( HeaderField.Value, 10 );
			}
		}

		/// <summary>
		/// Gets the transfer encoding used in the message body.
		/// </summary>
		private void GetTransferEncoding( )
		{
			SoapHeaderField HeaderField = m_Fields.Find( SoapHeader.TransferEncodingFieldName );
			
			if( HeaderField != null )
			{
				string Encoding = HeaderField.Value;

				if( String.Compare( Encoding, SoapHeader.TransferEncodingChunked, true, CultureInfo.InvariantCulture ) == 0 )
				{
					m_Chunked = true;
				}
			}
		}
		
		/// <summary>
		/// Finds the first occurrence of the carriage return character.
		/// </summary>
		/// <param name="bytes"></param>
		/// <returns></returns>
		private static int FindCarriageReturn( byte[ ] bytes )
		{
			int Index = -1;
			
			for( int i = 0; i < bytes.Length; ++i )
			{
				if( bytes[ i ] == CR )
				{
					Index = i;
					break;
				}
			}
			
			return Index;
		}
	}
}
